(bug 44459) Implement mw.message.text():
authorMatthew Flaschen <mflaschen@wikimedia.org>
Tue, 12 Feb 2013 04:16:08 +0000 (23:16 -0500)
committerNiklas Laxström <niklas.laxstrom@gmail.com>
Tue, 12 Feb 2013 08:26:06 +0000 (08:26 +0000)
* Change default message format in mediawiki.js from plain to text (this includes behavior of mw.msg shorthand).
* Code changes are primarily in jqueryMsg, with minor supporting changes in mediawiki.
* Organize tests by public entrypoint (mediawiki or mediawiki.jqueryMsg).
* Additional tests and assertions for all public entry points, for new and existing behavior.
* Convenience test assertion methods
* Add explanatory comments for new and existing code.
* Add setting to jqueryMsg for format, defaulting to parse, to preserve existing behavior for direct callers to jqueryMsg's public API.
* Since there are now two ways of constructing the abstract syntax tree, add a new parameter to the cache's key scheme.
* Minor formatting

Change-Id: I0d220692262356a12e2f1c0ce30cf6f090428332

resources/mediawiki/mediawiki.jqueryMsg.js
resources/mediawiki/mediawiki.js
tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.test.js

index 67a63ca..183b525 100644 (file)
                                'SITENAME' : mw.config.get( 'wgSiteName' )
                        },
                        messages : mw.messages,
-                       language : mw.language
+                       language : mw.language,
+
+                       // Same meaning as in mediawiki.js.
+                       //
+                       // Only 'text', 'parse', and 'escaped' are supported, and the
+                       // actual escaping for 'escaped' is done by other code (generally
+                       // through jqueryMsg).
+                       //
+                       // However, note that this default only
+                       // applies to direct calls to jqueryMsg. The default for mediawiki.js itself
+                       // is 'text', including when it uses jqueryMsg.
+                       format: 'parse'
+
                };
 
        /**
         * @return {Function} function suitable for assigning to window.gM
         */
        mw.jqueryMsg.getMessageFunction = function ( options ) {
-               var failableParserFn = getFailableParserFn( options );
+               var failableParserFn = getFailableParserFn( options ),
+                       format;
+
+               if ( options && options.format !== undefined ) {
+                       format = options.format;
+               } else {
+                       format = parserDefaults.format;
+               }
+
                /**
                 * N.B. replacements are variadic arguments or an array in second parameter. In other words:
                 *    somefunction(a, b, c, d)
                 * @return {string} Rendered HTML.
                 */
                return function () {
-                       return failableParserFn( arguments ).html();
+                       var failableResult = failableParserFn( arguments );
+                       if ( format === 'text' || format === 'escaped' ) {
+                               return failableResult.text();
+                       } else {
+                               return failableResult.html();
+                       }
                };
        };
 
         */
        mw.jqueryMsg.parser = function ( options ) {
                this.settings = $.extend( {}, parserDefaults, options );
+               this.settings.onlyCurlyBraceTransform = ( this.settings.format === 'text' || this.settings.format === 'escaped' );
+
                this.emitter = new mw.jqueryMsg.htmlEmitter( this.settings.language, this.settings.magic );
        };
 
        mw.jqueryMsg.parser.prototype = {
-               // cache, map of mediaWiki message key to the AST of the message. In most cases, the message is a string so this is identical.
-               // (This is why we would like to move this functionality server-side).
+               /**
+                * Cache mapping MediaWiki message keys and the value onlyCurlyBraceTransform, to the AST of the message.
+                *
+                * In most cases, the message is a string so this is identical.
+                * (This is why we would like to move this functionality server-side).
+                *
+                * The two parts of the key are separated by colon.  For example:
+                *
+                * "message-key:true": ast
+                *
+                * if they key is "message-key" and onlyCurlyBraceTransform is true.
+                *
+                * This cache is shared by all instances of mw.jqueryMsg.parser.
+                *
+                * @static
+                */
                astCache: {},
 
                /**
                 * @return {String|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing
                 */
                getAst: function ( key ) {
-                       if ( this.astCache[ key ] === undefined ) {
-                               var wikiText = this.settings.messages.get( key );
+                       var cacheKey = [key, this.settings.onlyCurlyBraceTransform].join( ':' ), wikiText;
+
+                       if ( this.astCache[ cacheKey ] === undefined ) {
+                               wikiText = this.settings.messages.get( key );
                                if ( typeof wikiText !== 'string' ) {
                                        wikiText = '\\[' + key + '\\]';
                                }
-                               this.astCache[ key ] = this.wikiTextToAst( wikiText );
+                               this.astCache[ cacheKey ] = this.wikiTextToAst( wikiText );
                        }
-                       return this.astCache[ key ];
+                       return this.astCache[ cacheKey ];
                },
-               /*
+
+               /**
                 * Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
                 *
                 * CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already.
                 */
                wikiTextToAst: function ( input ) {
                        var pos,
-                               regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, backslash, anyCharacter,
-                               escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
+                               regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets,
+                               backslash, anyCharacter, escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
                                whitespace, dollar, digits,
                                openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openLink, closeLink, templateName, pipe, colon,
                                templateContents, openTemplate, closeTemplate,
-                               nonWhitespaceExpression, paramExpression, expression, result;
+                               nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result;
 
                        // Indicates current position in input as we parse through it.
                        // Shared among all parsing functions below.
                        regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ );
                        regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/);
                        regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/);
+                       regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ );
                        backslash = makeStringParser( '\\' );
                        anyCharacter = makeRegexParser( /^./ );
                        function escapedLiteral() {
                        ] );
                        // Used to define "literals" without spaces, in space-delimited situations
                        function literalWithoutSpace() {
-                                var result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
-                                return result === null ? null : result.join('');
+                               var result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
+                               return result === null ? null : result.join('');
                        }
                        // Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default
                        // it is not a literal in the parameter
                        function literalWithoutBar() {
-                                var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
-                                return result === null ? null : result.join('');
+                               var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
+                               return result === null ? null : result.join('');
                        }
 
                        // Used for wikilink page names.  Like literalWithoutBar, but
                        }
 
                        function literal() {
-                                var result = nOrMore( 1, escapedOrRegularLiteral )();
-                                return result === null ? null : result.join('');
+                               var result = nOrMore( 1, escapedOrRegularLiteral )();
+                               return result === null ? null : result.join('');
                        }
+
+                       function curlyBraceTransformExpressionLiteral() {
+                               var result = nOrMore( 1, regularLiteralWithSquareBrackets )();
+                               return result === null ? null : result.join('');
+                       }
+
                        whitespace = makeRegexParser( /^\s+/ );
                        dollar = makeStringParser( '$' );
                        digits = makeRegexParser( /^\d+/ );
                                literal
                        ] );
 
-                       function start() {
-                               var result = nOrMore( 0, expression )();
+                       // Used when only {{-transformation is wanted, for 'text'
+                       // or 'escaped' formats
+                       curlyBraceTransformExpression = choice( [
+                               template,
+                               replacement,
+                               curlyBraceTransformExpressionLiteral
+                       ] );
+
+
+                       /**
+                        * Starts the parse
+                        *
+                        * @param {Function} rootExpression root parse function
+                        */
+                       function start( rootExpression ) {
+                               var result = nOrMore( 0, rootExpression )();
                                if ( result === null ) {
                                        return null;
                                }
                        // everything above this point is supposed to be stateless/static, but
                        // I am deferring the work of turning it into prototypes & objects. It's quite fast enough
                        // finally let's do some actual work...
-                       result = start();
+
+                       // If you add another possible rootExpression, you must update the astCache key scheme.
+                       result = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression );
 
                        /*
                         * For success, the p must have gotten to the end of the input
        // Replace the default message parser with jqueryMsg
        oldParser = mw.Message.prototype.parser;
        mw.Message.prototype.parser = function () {
+               var messageFunction;
+
                // TODO: should we cache the message function so we don't create a new one every time? Benchmark this maybe?
                // Caching is somewhat problematic, because we do need different message functions for different maps, so
                // we'd have to cache the parser as a member of this.map, which sounds a bit ugly.
                        // Fall back to mw.msg's simple parser
                        return oldParser.apply( this );
                }
-               var messageFunction = mw.jqueryMsg.getMessageFunction( { 'messages': this.map } );
+
+               messageFunction = mw.jqueryMsg.getMessageFunction( {
+                       'messages': this.map,
+                       // For format 'escaped', escaping part is handled by mediawiki.js
+                       'format': this.format
+               } );
                return messageFunction( this.key, this.parameters );
        };
 
index 658487e..68a3a09 100644 (file)
@@ -127,7 +127,7 @@ var mw = ( function ( $, undefined ) {
         * @return Message
         */
        function Message( map, key, parameters ) {
-               this.format = 'plain';
+               this.format = 'text';
                this.map = map;
                this.key = key;
                this.parameters = parameters === undefined ? [] : slice.call( parameters );
@@ -136,9 +136,13 @@ var mw = ( function ( $, undefined ) {
 
        Message.prototype = {
                /**
-                * Simple message parser, does $N replacement and nothing else.
+                * Simple message parser, does $N replacement, HTML-escaping (only for
+                * 'escaped' format), and nothing else.
+                *
                 * This may be overridden to provide a more complex message parser.
                 *
+                * The primary override is in mediawiki.jqueryMsg.
+                *
                 * This function will not be called for nonexistent messages.
                 */
                parser: function () {
@@ -173,14 +177,14 @@ var mw = ( function ( $, undefined ) {
 
                        if ( !this.exists() ) {
                                // Use <key> as text if key does not exist
-                               if ( this.format !== 'plain' ) {
-                                       // format 'escape' and 'parse' need to have the brackets and key html escaped
+                               if ( this.format === 'escaped' || this.format === 'parse' ) {
+                                       // format 'escaped' and 'parse' need to have the brackets and key html escaped
                                        return mw.html.escape( '<' + this.key + '>' );
                                }
                                return '<' + this.key + '>';
                        }
 
-                       if ( this.format === 'plain' || this.format === 'parse' ) {
+                       if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
                                text = this.parser();
                        }
 
@@ -193,7 +197,12 @@ var mw = ( function ( $, undefined ) {
                },
 
                /**
-                * Changes format to parse and converts message to string
+                * Changes format to 'parse' and converts message to string
+                *
+                * If jqueryMsg is loaded, this parses the message text from wikitext
+                * (where supported) to HTML
+                *
+                * Otherwise, it is equivalent to plain.
                 *
                 * @return {string} String form of parsed message
                 */
@@ -203,7 +212,10 @@ var mw = ( function ( $, undefined ) {
                },
 
                /**
-                * Changes format to plain and converts message to string
+                * Changes format to 'plain' and converts message to string
+                *
+                * This substitutes parameters, but otherwise does not change the
+                * message text.
                 *
                 * @return {string} String form of plain message
                 */
@@ -213,7 +225,23 @@ var mw = ( function ( $, undefined ) {
                },
 
                /**
-                * Changes the format to html escaped and converts message to string
+                * Changes format to 'text' and converts message to string
+                *
+                * If jqueryMsg is loaded, {{-transformation is done where supported
+                * (such as {{plural:}}, {{gender:}}, {{int:}}).
+                *
+                * Otherwise, it is equivalent to plain.
+                */
+               text: function () {
+                       this.format = 'text';
+                       return this.toString();
+               },
+
+               /**
+                * Changes the format to 'escaped' and converts message to string
+                *
+                * This is equivalent to using the 'text' format (see text method), then
+                * HTML-escaping the output.
                 *
                 * @return {string} String form of html escaped message
                 */
index 6e9379e..6c2a2d4 100644 (file)
@@ -1,6 +1,7 @@
 ( function ( mw, $ ) {
 
-var mwLanguageCache = {}, oldGetOuterHtml, formatnumTests, specialCharactersPageName;
+var mwLanguageCache = {}, oldGetOuterHtml, formatnumTests, specialCharactersPageName,
+       expectedListUsers, expectedEntrypoints;
 
 QUnit.module( 'mediawiki.jqueryMsg', QUnit.newMwEnvironment( {
        setup: function () {
@@ -17,15 +18,36 @@ QUnit.module( 'mediawiki.jqueryMsg', QUnit.newMwEnvironment( {
                };
 
                // Messages that are reused in multiple tests
-               // They are also all part of regression tests based on actual extensions.  The actual messages have the same key,
-               // but without jquerymsg-test-.
                mw.messages.set( {
-                       'jquerymsg-test-pagetriage-del-talk-page-notify-summary': 'Notifying author of deletion nomination for [[$1]]',
-                       'jquerymsg-test-categorytree-collapse-bullet': '[<b>−</b>]',
-                       'jquerymsg-test-wikieditor-toolbar-help-content-signature-result': '<a href=\'#\' title=\'{{#special:mypage}}\'>Username</a> (<a href=\'#\' title=\'{{#special:mytalk}}\'>talk</a>)'
+                       // The values for gender are not significant,
+                       // what matters is which of the values is choosen by the parser
+                       'gender-msg': '$1: {{GENDER:$2|blue|pink|green}}',
+
+                       'plural-msg': 'Found $1 {{PLURAL:$1|item|items}}',
+
+                       // Assume the grammar form grammar_case_foo is not valid in any language
+                       'grammar-msg': 'Przeszukaj {{GRAMMAR:grammar_case_foo|{{SITENAME}}}}',
+
+                       'formatnum-msg': '{{formatnum:$1}}',
+
+                       'portal-url': 'Project:Community portal',
+                       'see-portal-url': '{{Int:portal-url}} is an important community page.',
+
+                       'jquerymsg-test-statistics-users': '注册[[Special:ListUsers|用户]]',
+
+                       'jquerymsg-test-version-entrypoints-index-php': '[https://www.mediawiki.org/wiki/Manual:index.php index.php]',
+
+                       'external-link-replace': 'Foo [$1 bar]'
                } );
 
                specialCharactersPageName = '"Who" wants to be a millionaire & live on \'Exotic Island\'?';
+
+               expectedListUsers = '注册' + $( '<a>' ).attr( {
+                       title: 'Special:ListUsers',
+                       href: mw.util.wikiGetlink( 'Special:ListUsers' )
+               } ).text( '用户' ).getOuterHtml();
+
+               expectedEntrypoints = '<a href="https://www.mediawiki.org/wiki/Manual:index.php">index.php</a>';
        },
        teardown: function () {
                mw.language = this.orgMwLangauge;
@@ -105,7 +127,6 @@ QUnit.test( 'Replace', 9, function ( assert ) {
                'HTMLElement[] arrays are preserved as raw html'
        );
 
-       mw.messages.set( 'external-link-replace', 'Foo [$1 bar]' );
        assert.equal(
                parser( 'external-link-replace', 'http://example.org/?x=y&z' ),
                'Foo <a href="http://example.org/?x=y&amp;z">bar</a>',
@@ -116,7 +137,6 @@ QUnit.test( 'Replace', 9, function ( assert ) {
 QUnit.test( 'Plural', 3, function ( assert ) {
        var parser = mw.jqueryMsg.getMessageFunction();
 
-       mw.messages.set( 'plural-msg', 'Found $1 {{PLURAL:$1|item|items}}' );
        assert.equal( parser( 'plural-msg', 0 ), 'Found 0 items', 'Plural test for english with zero as count' );
        assert.equal( parser( 'plural-msg', 1 ), 'Found 1 item', 'Singular test for english' );
        assert.equal( parser( 'plural-msg', 2 ), 'Found 2 items', 'Plural test for english' );
@@ -128,10 +148,6 @@ QUnit.test( 'Gender', 11, function ( assert ) {
        var user = mw.user,
                parser = mw.jqueryMsg.getMessageFunction();
 
-       // The values here are not significant,
-       // what matters is which of the values is choosen by the parser
-       mw.messages.set( 'gender-msg', '$1: {{GENDER:$2|blue|pink|green}}' );
-
        user.options.set( 'gender', 'male' );
        assert.equal(
                parser( 'gender-msg', 'Bob', 'male' ),
@@ -199,8 +215,6 @@ QUnit.test( 'Gender', 11, function ( assert ) {
 QUnit.test( 'Grammar', 2, function ( assert ) {
        var parser = mw.jqueryMsg.getMessageFunction();
 
-       // Assume the grammar form grammar_case_foo is not valid in any language
-       mw.messages.set( 'grammar-msg', 'Przeszukaj {{GRAMMAR:grammar_case_foo|{{SITENAME}}}}' );
        assert.equal( parser( 'grammar-msg' ), 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar Test with sitename' );
 
        mw.messages.set( 'grammar-msg-wrong-syntax', 'Przeszukaj {{GRAMMAR:grammar_case_xyz}}' );
@@ -230,7 +244,6 @@ QUnit.test( 'Match PHP parser', mw.libs.phpParserData.tests.length, function ( a
 
 QUnit.test( 'Links', 6, function ( assert ) {
        var parser = mw.jqueryMsg.getMessageFunction(),
-               expectedListUsers,
                expectedDisambiguationsText,
                expectedMultipleBars,
                expectedSpecialCharacters;
@@ -240,13 +253,6 @@ QUnit.test( 'Links', 6, function ( assert ) {
         the bold was removed because it is not yet implemented.
        */
 
-       mw.messages.set( 'jquerymsg-test-statistics-users', '注册[[Special:ListUsers|用户]]' );
-
-       expectedListUsers = '注册' + $( '<a>' ).attr( {
-               title: 'Special:ListUsers',
-               href: mw.util.wikiGetlink( 'Special:ListUsers' )
-       } ).text( '用户' ).getOuterHtml();
-
        assert.equal(
                parser( 'jquerymsg-test-statistics-users' ),
                expectedListUsers,
@@ -265,10 +271,9 @@ QUnit.test( 'Links', 6, function ( assert ) {
                'Wikilink without pipe'
        );
 
-       mw.messages.set( 'jquerymsg-test-version-entrypoints-index-php', '[https://www.mediawiki.org/wiki/Manual:index.php index.php]' );
        assert.equal(
                parser( 'jquerymsg-test-version-entrypoints-index-php' ),
-               '<a href="https://www.mediawiki.org/wiki/Manual:index.php">index.php</a>',
+               expectedEntrypoints,
                'External link'
        );
 
@@ -304,36 +309,74 @@ QUnit.test( 'Links', 6, function ( assert ) {
        );
 });
 
-// Output for format plain when calling main (mediawiki.js) API.
-// We're testing here to ensure our monkey-patching of mw.Message.prototype.parser doesn't
-// cause breakage.
+// Tests that {{-transformation vs. general parsing are done as requested
+QUnit.test( 'Curly brace transformation', 14, function ( assert ) {
+       var formatText, formatParse, oldUserLang;
+
+       oldUserLang = mw.config.get( 'wgUserLanguage' ) ;
+
+       formatText= mw.jqueryMsg.getMessageFunction( {
+               format: 'text'
+       } );
+
+       formatParse = mw.jqueryMsg.getMessageFunction( {
+               format: 'parse'
+       } );
+
+       // When the expected result is the same in both modes
+       function assertBothModes( parserArguments, expectedResult, assertMessage) {
+               assert.equal( formatText.apply( null, parserArguments ), expectedResult, assertMessage + ' when format is \'text\'' );
+               assert.equal( formatParse.apply( null, parserArguments ), expectedResult, assertMessage + ' when format is \'parse\'' );
+       }
+
+       assertBothModes( ['gender-msg', 'Bob', 'male'], 'Bob: blue', 'gender is resolved' );
+
+       assertBothModes( ['plural-msg', 5], 'Found 5 items', 'plural is resolved' );
+
+       assertBothModes( ['grammar-msg'], 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'grammar is resolved' );
+
+       mw.config.set( 'wgUserLanguage', 'en' ) ;
+       assertBothModes( ['formatnum-msg', '987654321.654321'], '987654321.654321', 'formatnum is resolved' );
 
-// Some of the tests use mw.msg, while others have mw.message(...).plain().  These two
-// syntaxes should have identical behavior.
-QUnit.test( 'Plain', 4, function ( assert ) {
+       // Test non-{{ wikitext, where behavior differs
+
+       // Wikilink
        assert.equal(
-               mw.message( 'jquerymsg-test-pagetriage-del-talk-page-notify-summary' ).plain(),
-               'Notifying author of deletion nomination for [[$1]]',
-               'Square brackets in plain with no parameters'
+               formatText( 'jquerymsg-test-statistics-users' ),
+               mw.messages.get( 'jquerymsg-test-statistics-users' ),
+               'Internal link message unchanged when format is \'text\''
        );
-
        assert.equal(
-               mw.msg( 'jquerymsg-test-pagetriage-del-talk-page-notify-summary', specialCharactersPageName ),
-               'Notifying author of deletion nomination for [[' + specialCharactersPageName + ']]',
-               'Square brackets in plain with one parameter'
+               formatParse( 'jquerymsg-test-statistics-users' ),
+               expectedListUsers,
+               'Internal link message parsed when format is \'parse\''
        );
 
+       // External link
+       assert.equal(
+               formatText( 'jquerymsg-test-version-entrypoints-index-php' ),
+               mw.messages.get( 'jquerymsg-test-version-entrypoints-index-php' ),
+               'External link message unchanged when format is \'text\''
+       );
        assert.equal(
-               mw.msg( 'jquerymsg-test-categorytree-collapse-bullet' ),
-               mw.messages.get( 'jquerymsg-test-categorytree-collapse-bullet' ),
-               'Message with single square brackets is not changed'
+               formatParse( 'jquerymsg-test-version-entrypoints-index-php' ),
+               expectedEntrypoints,
+               'External link message processed when format is \'parse\''
        );
 
+       // External link with parameter
+       assert.equal(
+               formatText( 'external-link-replace', 'http://example.com' ),
+               'Foo [http://example.com bar]',
+               'External link message only substitutes parameter when format is \'text\''
+       );
        assert.equal(
-               mw.message( 'jquerymsg-test-wikieditor-toolbar-help-content-signature-result' ).plain(),
-               mw.messages.get( 'jquerymsg-test-wikieditor-toolbar-help-content-signature-result' ),
-               'HTML message with curly braces is not changed'
+               formatParse( 'external-link-replace', 'http://example.com' ),
+               'Foo <a href="http://example.com">bar</a>',
+               'External link message processed when format is \'parse\''
        );
+
+       mw.config.set( 'wgUserLanguage', oldUserLang );
 } );
 
 QUnit.test( 'Int', 4, function ( assert ) {
@@ -357,8 +400,6 @@ QUnit.test( 'Int', 4, function ( assert ) {
                'Link with nested message'
        );
 
-       mw.messages.set( 'portal-url', 'Project:Community portal' );
-       mw.messages.set( 'see-portal-url', '{{Int:portal-url}} is an important community page.' );
        assert.equal(
                parser( 'see-portal-url' ),
                'Project:Community portal is an important community page.',
@@ -385,7 +426,7 @@ QUnit.test( 'Int', 4, function ( assert ) {
 
 // Tests that getMessageFunction is used for non-plain messages with curly braces or
 // square brackets, but not otherwise.
-QUnit.test( 'mw.msg()', 22, function ( assert ) {
+QUnit.test( 'mw.Message.prototype.parser monkey-patch', 22, function ( assert ) {
        var oldGMF, outerCalled, innerCalled;
 
        mw.messages.set( {
@@ -487,7 +528,6 @@ formatnumTests = [
 ];
 
 QUnit.test( 'formatnum', formatnumTests.length, function ( assert ) {
-       mw.messages.set( 'formatnum-msg', '{{formatnum:$1}}' );
        mw.messages.set( 'formatnum-msg-int', '{{formatnum:$1|R}}' );
        $.each( formatnumTests, function ( i, test ) {
                QUnit.stop();
index dc7bd0a..ec061a6 100644 (file)
@@ -1,6 +1,31 @@
 ( function ( mw, $ ) {
 
-QUnit.module( 'mediawiki', QUnit.newMwEnvironment() );
+var specialCharactersPageName;
+
+
+// Since QUnitTestResources.php loads both mediawiki and mediawiki.jqueryMsg as
+// dependencies, this only tests the monkey-patched behavior with the two of them combined.
+
+// See mediawiki.jqueryMsg.test.js for unit tests for jqueryMsg-specific functionality.
+
+QUnit.module( 'mediawiki', QUnit.newMwEnvironment( {
+       setup: function () {
+               // Messages used in multiple tests
+               mw.messages.set( {
+                       'other-message': 'Other Message',
+                       'mediawiki-test-pagetriage-del-talk-page-notify-summary': 'Notifying author of deletion nomination for [[$1]]',
+                       'gender-plural-msg': '{{GENDER:$1|he|she|they}} {{PLURAL:$2|is|are}} awesome',
+                       'grammar-msg': 'Przeszukaj {{GRAMMAR:grammar_case_foo|{{SITENAME}}}}',
+                       'formatnum-msg': '{{formatnum:$1}}',
+                       'int-msg': 'Some {{int:other-message}}'
+               } );
+
+               // For formatnum tests
+               mw.config.set( 'wgUserLanguage', 'en' );
+
+               specialCharactersPageName = '"Who" wants to be a millionaire & live on \'Exotic Island\'?';
+       }
+} ) );
 
 QUnit.test( 'Initial check', 8, function ( assert ) {
        assert.ok( window.jQuery, 'jQuery defined' );
@@ -77,8 +102,17 @@ QUnit.test( 'mw.config', 1, function ( assert ) {
        assert.ok( mw.config instanceof mw.Map, 'mw.config instance of mw.Map' );
 });
 
-QUnit.test( 'mw.message & mw.messages', 20, function ( assert ) {
-       var goodbye, hello, pluralMessage;
+QUnit.test( 'mw.message & mw.messages', 54, function ( assert ) {
+       var goodbye, hello;
+
+       // Convenience method for asserting the same result for multiple formats
+       function assertMultipleFormats( messageArguments, formats, expectedResult, assertMessage) {
+               var len = formats.length, format, i;
+               for ( i = 0; i < len; i++ ) {
+                       format = formats[i];
+                       assert.equal( mw.message.apply( null, messageArguments )[format](), expectedResult, assertMessage + ' when format is ' + format);
+               }
+       }
 
        assert.ok( mw.messages, 'messages defined' );
        assert.ok( mw.messages instanceof mw.Map, 'mw.messages instance of mw.Map' );
@@ -86,7 +120,9 @@ QUnit.test( 'mw.message & mw.messages', 20, function ( assert ) {
 
        hello = mw.message( 'hello' );
 
-       assert.equal( hello.format, 'plain', 'Message property "format" defaults to "plain"' );
+       // https://bugzilla.wikimedia.org/show_bug.cgi?id=44459
+       assert.equal( hello.format, 'text', 'Message property "format" defaults to "text"' );
+
        assert.strictEqual( hello.map, mw.messages, 'Message property "map" defaults to the global instance in mw.messages' );
        assert.equal( hello.key, 'hello', 'Message property "key" (currect key)' );
        assert.deepEqual( hello.parameters, [], 'Message property "parameters" defaults to an empty array' );
@@ -100,29 +136,62 @@ QUnit.test( 'mw.message & mw.messages', 20, function ( assert ) {
        assert.equal( hello.escaped(), 'Hello &lt;b&gt;awesome&lt;/b&gt; world', 'Message.escaped returns the escaped message' );
        assert.equal( hello.format, 'escaped', 'Message.escaped correctly updated the "format" property' );
 
+       assert.ok( mw.messages.set( 'escaped-with-curly-brace', '"{{SITENAME}}" is the home of {{int:other-message}}' ) );
+       assert.equal( mw.message( 'escaped-with-curly-brace' ).escaped(), mw.html.escape( '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message' ), 'Escaped format works correctly for curly brace message' );
+
+       assert.ok( mw.messages.set( 'escaped-with-square-brackets', 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]' ) );
+       assert.equal( mw.message( 'escaped-with-square-brackets' ).escaped(), 'Visit the [[Project:Community portal|community portal]] &amp; [[Project:Help desk|help desk]]', 'Escaped format works correctly for square bracket message' );
+
        hello.parse();
        assert.equal( hello.format, 'parse', 'Message.parse correctly updated the "format" property' );
 
        hello.plain();
        assert.equal( hello.format, 'plain', 'Message.plain correctly updated the "format" property' );
 
+       hello.text();
+       assert.equal( hello.format, 'text', 'Message.text correctly updated the "format" property' );
+
        assert.strictEqual( hello.exists(), true, 'Message.exists returns true for existing messages' );
 
        goodbye = mw.message( 'goodbye' );
        assert.strictEqual( goodbye.exists(), false, 'Message.exists returns false for nonexistent messages' );
 
-       assert.equal( goodbye.plain(), '<goodbye>', 'Message.toString returns plain <key> if format is "plain" and key does not exist' );
+       assertMultipleFormats( ['goodbye'], ['plain', 'text'], '<goodbye>', 'Message.toString returns <key> if key does not exist' );
        // bug 30684
-       assert.equal( goodbye.escaped(), '&lt;goodbye&gt;', 'Message.toString returns properly escaped &lt;key&gt; if format is "escaped" and key does not exist' );
+       assertMultipleFormats( ['goodbye'], ['parse', 'escaped'], '&lt;goodbye&gt;', 'Message.toString returns properly escaped &lt;key&gt; if key does not exist' );
+
+       assert.ok( mw.messages.set( 'plural-test-msg', 'There {{PLURAL:$1|is|are}} $1 {{PLURAL:$1|result|results}}' ), 'mw.messages.set: Register' );
+       assertMultipleFormats( ['plural-test-msg', 6], ['text', 'parse', 'escaped'], 'There are 6 results', 'plural get resolved' );
+       assert.equal( mw.message( 'plural-test-msg', 6 ).plain(), 'There {{PLURAL:6|is|are}} 6 {{PLURAL:6|result|results}}', 'Parameter is substituted but plural is not resolved in plain' );
+
+       assertMultipleFormats( ['mediawiki-test-pagetriage-del-talk-page-notify-summary'], ['plain', 'text'], mw.messages.get( 'mediawiki-test-pagetriage-del-talk-page-notify-summary' ), 'Double square brackets with no parameters unchanged' );
+
+       assertMultipleFormats( ['mediawiki-test-pagetriage-del-talk-page-notify-summary', specialCharactersPageName], ['plain', 'text'], 'Notifying author of deletion nomination for [[' + specialCharactersPageName + ']]', 'Double square brackets with one parameter' );
+
+       assert.equal( mw.message( 'mediawiki-test-pagetriage-del-talk-page-notify-summary', specialCharactersPageName ).escaped(), 'Notifying author of deletion nomination for [[' + mw.html.escape( specialCharactersPageName ) + ']]', 'Double square brackets with one parameter, when escaped' );
+
+
+       assert.ok( mw.messages.set( 'mediawiki-test-categorytree-collapse-bullet', '[<b>−</b>]' ), 'mw.messages.set: Register' );
+       assert.equal( mw.message( 'mediawiki-test-categorytree-collapse-bullet' ).plain(), mw.messages.get( 'mediawiki-test-categorytree-collapse-bullet' ), 'Single square brackets unchanged in plain mode' );
 
-       assert.ok( mw.messages.set( 'pluraltestmsg', 'There {{PLURAL:$1|is|are}} $1 {{PLURAL:$1|result|results}}' ), 'mw.messages.set: Register' );
-       pluralMessage = mw.message( 'pluraltestmsg' , 6 );
-       assert.equal( pluralMessage.plain(), 'There are 6 results', 'plural get resolved when format is plain' );
-       assert.equal( pluralMessage.parse(), 'There are 6 results', 'plural get resolved when format is parse' );
+       assert.ok( mw.messages.set( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result', '<a href=\'#\' title=\'{{#special:mypage}}\'>Username</a> (<a href=\'#\' title=\'{{#special:mytalk}}\'>talk</a>)' ) );
+       assert.equal( mw.message( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result' ).plain(), mw.messages.get( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result' ), 'HTML message with curly braces is not changed in plain mode' );
 
+       assertMultipleFormats( ['gender-plural-msg', 'male', 1], ['text', 'parse', 'escaped'], 'he is awesome', 'Gender and plural are resolved' );
+       assert.equal( mw.message( 'gender-plural-msg', 'male', 1 ).plain(), '{{GENDER:male|he|she|they}} {{PLURAL:1|is|are}} awesome', 'Parameters are substituted, but gender and plural are not resolved in plain mode' );
+
+       assert.equal( mw.message( 'grammar-msg' ).plain(), mw.messages.get( 'grammar-msg' ), 'Grammar is not resolved in plain mode' );
+       assertMultipleFormats( ['grammar-msg'], ['text', 'parse'], 'Przeszukaj ' + mw.config.get( 'wgSiteName' ), 'Grammar is resolved' );
+       assert.equal( mw.message( 'grammar-msg' ).escaped(), 'Przeszukaj ' + mw.html.escape( mw.config.get( 'wgSiteName' ) ), 'Grammar is resolved in escaped mode' );
+
+       assertMultipleFormats( ['formatnum-msg', '987654321.654321'], ['text', 'parse', 'escaped'], '987654321.654321', 'formatnum is resolved' );
+       assert.equal( mw.message( 'formatnum-msg' ).plain(), mw.messages.get( 'formatnum-msg' ), 'formatnum is not resolved in plain mode' );
+
+       assertMultipleFormats( ['int-msg'], ['text', 'parse', 'escaped'], 'Some Other Message', 'int is resolved' );
+       assert.equal( mw.message( 'int-msg' ).plain(), mw.messages.get( 'int-msg' ), 'int is not resolved in plain mode' );
 });
 
-QUnit.test( 'mw.msg', 11, function ( assert ) {
+QUnit.test( 'mw.msg', 14, function ( assert ) {
        assert.ok( mw.messages.set( 'hello', 'Hello <b>awesome</b> world' ), 'mw.messages.set: Register' );
        assert.equal( mw.msg( 'hello' ), 'Hello <b>awesome</b> world', 'Gets message with default options (existing message)' );
        assert.equal( mw.msg( 'goodbye' ), '<goodbye>', 'Gets message with default options (nonexistent message)' );
@@ -132,11 +201,17 @@ QUnit.test( 'mw.msg', 11, function ( assert ) {
        assert.equal( mw.msg( 'plural-item', 0 ), 'Found 0 items', 'Apply plural for count 0' );
        assert.equal( mw.msg( 'plural-item', 1 ), 'Found 1 item', 'Apply plural for count 1' );
 
-       assert.ok( mw.messages.set('gender-plural-msg' , '{{GENDER:$1|he|she|they}} {{PLURAL:$2|is|are}} awesome' ) );
+       assert.equal( mw.msg( 'mediawiki-test-pagetriage-del-talk-page-notify-summary', specialCharactersPageName ), 'Notifying author of deletion nomination for [[' + specialCharactersPageName + ']]', 'Double square brackets in mw.msg one parameter' );
+
        assert.equal( mw.msg( 'gender-plural-msg', 'male', 1 ), 'he is awesome', 'Gender test for male, plural count 1' );
        assert.equal( mw.msg( 'gender-plural-msg', 'female', '1' ), 'she is awesome', 'Gender test for female, plural count 1' );
        assert.equal( mw.msg( 'gender-plural-msg', 'unknown', 10 ), 'they are awesome', 'Gender test for neutral, plural count 10' );
 
+       assert.equal( mw.msg( 'grammar-msg' ), 'Przeszukaj ' + mw.config.get( 'wgSiteName'), 'Grammar is resolved' );
+
+       assert.equal( mw.msg( 'formatnum-msg', '987654321.654321' ), '987654321.654321', 'formatnum is resolved' );
+
+       assert.equal( mw.msg( 'int-msg' ), 'Some Other Message', 'int is resolved' );
 });
 
 /**